library lib_network;

import 'dart:convert';
import 'dart:io';

import 'package:dio/dio.dart';
import 'package:dio/io.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_proxy_native/flutter_proxy_native.dart';
import 'package:lib_base/logger/logger.dart';
import 'package:lib_network/api_result.dart';
import 'package:lib_network/data_repositories/service_address_repository.dart';
import 'package:lib_network/extension/dio_error_extension.dart';
import 'package:lib_network/http_service_datasource.dart';
import 'package:lib_network/interceptors/http_log_interceptor.dart';
import 'package:lib_network/interceptors/log_options.dart';
import 'package:lib_network/models/service_address_model.dart';
import 'package:rxdart/rxdart.dart';

class HttpService {
  static final HttpService instance = HttpService._internal();

  late final Dio _dio;

  final _flutterProxyPlugin = FlutterProxyNative();

  HttpServiceDataSource? dataSource;

  Future<String?> get h5BaseUrl async {
    ServiceAddressModel? serviceAddressModel =
        await ServiceAddressRepository.instance.getData();
    return serviceAddressModel?.h5BaseUrl;
  }

  Future<String?> get apiBaseUrl async {
    ServiceAddressModel? serviceAddressModel =
        await ServiceAddressRepository.instance.getData();
    return serviceAddressModel?.apiBaseUrl;
  }

  static String errorMessage(dynamic error) {
    return error is DioException ? error.localizedMessage : error.toString();
  }

  static void printErrorMessage(dynamic error) {
    if (kDebugMode) {
      print(errorMessage(error));
    }
  }

  HttpService._internal() {
    var options = BaseOptions(
      connectTimeout: const Duration(seconds: 5),
      receiveTimeout: const Duration(seconds: 3),
      responseType: ResponseType.json,
    );
    _dio = Dio(options);

    // 添加日志
    _dio.interceptors.add(HttpLogInterceptor(
      logOptions: LogOptions.formatRequestAscURL &
          LogOptions.requestHeaders &
          LogOptions.responseHeaders &
          LogOptions.error,
      logPrint: _logPrint,
    ));
  }

  void _logPrint(Object? object) {
    Logger.instance.d(object);
  }

  Future<void> addBaseUrlAndAuth(Dio dio) async {
    dio.options.baseUrl = await apiBaseUrl ?? '';

    Map<String, String>? values = await dataSource?.getAccessToken(this);
    if (values != null) {
      if (values['accessTokenType'] != null) {
        String? accessTokenType = values['accessTokenType'];
        if (accessTokenType != null) {
          accessTokenType = '$accessTokenType ';
        }

        dio.options.headers['Authorization'] =
            '${accessTokenType ?? ''}${values['accessToken']}';
      }
    } else {
      dio.options.headers.remove('Authorization');
    }
  }

  Future<void> addHeaders(Dio dio, Map<String, dynamic>? headers) async {
    Map<String, String>? defaultHeaders = await dataSource?.getHeaders(this);

    if (defaultHeaders == null) {
      return;
    }

    dio.options.headers.addAll(defaultHeaders);

    if (headers != null) {
      dio.options.headers.addAll(headers);
    }
  }

  Future<Response<String>> _request<T>(
    String url,
    DataFromJson<T> dataFromJson, {
    String method = 'GET',
    dynamic data,
    Map<String, dynamic>? queryParameters,
    String? contentType,
    Map<String, dynamic>? headers,
    CancelToken? cancelToken,
    APIResultDecoder<T>? jsonDecoder,
    Dio? dio,
  }) async {
    Dio httpDio = dio ?? HttpService.instance._dio;

    var httpClientAdapter = httpDio.httpClientAdapter;
    if (httpClientAdapter is IOHttpClientAdapter) {
      List<ByteData>? pemDataList =
          await HttpService.instance.dataSource?.secCertificates();

      String? proxy;
      try {
        proxy = await _flutterProxyPlugin.getSystemProxy();
      } catch (e) {
        proxy = null;
      }

      httpClientAdapter.createHttpClient = () {
        HttpClient httpClient;

        if (pemDataList != null && pemDataList.isNotEmpty) {
          SecurityContext sc = SecurityContext();
          httpClient = HttpClient(context: sc);

          for (var element in pemDataList) {
            sc.setTrustedCertificatesBytes(element.buffer.asUint8List());
          }

          httpClient.badCertificateCallback = (cert, host, port) {
            return pemDataList.every((element) =>
                cert.pem == utf8.decode(element.buffer.asUint8List()));
          };
        } else {
          httpClient = HttpClient();
        }

        httpClient.findProxy = (uri) =>
            proxy != null && proxy.isNotEmpty && proxy != 'null:null'
                ? "PROXY $proxy;"
                : 'DIRECT';

        return httpClient;
      };
    }

    await addBaseUrlAndAuth(httpDio);

    await addHeaders(httpDio, headers);

    httpDio.options.contentType = contentType ??
        ContentType(
          'application',
          'x-www-form-urlencoded',
          charset: 'utf-8',
        ).value;

    if (!httpDio.interceptors
        .any((element) => element is InterceptorsWrapper)) {
      httpDio.interceptors.add(
        InterceptorsWrapper(
          onError: (e, handler) {
            Response? response = e.response;

            if (response != null && response.statusCode == 401) {
              String? data = response.data as String?;
              Map<String, dynamic>? dataMap;

              if (data != null) {
                dataMap = json.decode(data) as Map<String, dynamic>?;

                if (dataMap?['code'] == 100004) {
                  dataSource?.refreshToken().then((result) {
                    if (!result) {
                      handler.next(e);
                      return;
                    }

                    _request(
                      url,
                      dataFromJson,
                      method: method,
                      data: data,
                      queryParameters: queryParameters,
                      contentType: contentType,
                      headers: headers,
                      cancelToken: cancelToken,
                      jsonDecoder: jsonDecoder,
                      dio: dio,
                    ).then((value) {
                      handler.resolve(value);
                    }).catchError((error) {
                      handler.next(e);
                    });
                  }).catchError((error) {
                    handler.next(e);
                  });

                  return;
                }
              }

              dataSource?.invalidToken(message: dataMap?['msg']);
            }

            handler.next(e);
          },
        ),
      );
    }

    return httpDio.request<String>(
      url,
      data: data,
      queryParameters: queryParameters,
      options: Options(method: method),
      cancelToken: cancelToken,
    );
  }

  Stream<APIResult<T>> request<T>(
    String url,
    DataFromJson<T> dataFromJson, {
    String method = 'GET',
    dynamic data,
    Map<String, dynamic>? queryParameters,
    String? contentType,
    Map<String, dynamic>? headers,
    CancelToken? cancelToken,
    APIResultDecoder<T>? jsonDecoder,
    Dio? dio,
  }) {
    const int maxRequestCount = 3;
    int requestCount = 0;

    return Rx.retryWhen(
      () => Rx.fromCallable(
        () {
          ++requestCount;

          return _request(
            url,
            dataFromJson,
            method: method,
            data: data,
            queryParameters: queryParameters,
            contentType: contentType,
            headers: headers,
            cancelToken: cancelToken,
            jsonDecoder: jsonDecoder,
            dio: dio,
          );
        },
      ),
      (error, stackTrace) {
        if (requestCount < maxRequestCount &&
            error is DioException &&
            [
              DioExceptionType.connectionTimeout,
              DioExceptionType.sendTimeout,
              DioExceptionType.receiveTimeout,
            ].contains(error.type)) {
          return Rx.timer(null, const Duration(seconds: 1));
        } else {
          throw error;
        }
      },
    ).map(
      (response) {
        String? data = response.data;
        if (data != null) {
          try {
            return APIResult.fromJson(jsonDecode(data), dataFromJson,
                jsonDecoder: jsonDecoder);
          } catch (e) {
            throw ArgumentError("Failed to map data to APIResult dictionary.");
          }
        } else {
          throw ArgumentError.notNull('Response.data');
        }
      },
    );
  }
}
